import * as act from '@/actions'
import { CaptureScreenshotService } from '@/common/capture_screenshot'
import * as CONSTANT from '@/common/constant'
import { evaluateScript } from '@/common/eval'
import log from '@/common/log'
import { player } from '@/runner/player'
import { PLAY_MODE } from '@/types/player'
import { activateTab } from '@/common/tab_utils'
import {
  compose,
  flatten,
  id,
  safeUpdateIn,
  strictParseBoolLike,
  withCountDown
} from '@/common/ts_utils'
import {
  cloneSerializableLocalStorage,
  delay,
  retry,
  ensureExtName,
  downloadByBlob,
  setIn,
  updateIn,
} from '@/common/utils'
import { getVarsInstance } from '@/common/varsFactory'
import Ext from '@/common/web_extension'
import { getPlayTab } from '@/ext/common/tab'
import { findMacroNodeWithCaseInsensitiveRelativePath } from '@/recomputed'
import { MacroResultStatus } from '@/types/macro_extra_data'
import { MacroStatus } from '@/types/macro'
import { getStorageManager } from '@/file_storage'
import store from '@/store'
import { callStack } from '@/runner/callStack'
import { stringifyToCSV, parseFromCSV } from '@/common/csv'
import { posix as pathPosix, win32 as pathWin32 } from '@/common/path';
import { vars } from './vars';
import { interpreter } from './interpreter'
import { renderLog } from '@/common/macro_log'
import {
  allWordsWithPosition,
  ocrMatchCenter,
  scaleOcrTextSearchMatch,
  searchTextInOCRResponse
} from '@/services/ocr'
import { convertOcrLanguageToTesseractLanguage, OCRLanguage } from '@/services/ocr/languages'
import { OcrHighlightType } from '@/services/ocr/types'
import { askBg, askPlayTab } from '@/ext/ipc'
import { getState, updateState } from '@/ext/common/global_state';
import { getOcrResponse, guardOcrSettings } from './ocr'
import config from '@/config'
import { withVisualHighlightHidden, } from './helper'
import { clearTimerForTimeoutStatus, startSendingTimeoutStatus } from './timeout_counter'

function waitForPageLoadComplete(tab: any): Promise<boolean> {
  return new Promise((resolve, reject) => {
    const timeout = 60 * 1000;
    const interval = 300;
    let elapsed = 0;
    const timer = setInterval(() => {
      elapsed += interval;
      if (elapsed > timeout) {
        clearInterval(timer);
        reject(new Error('timeout'));
      }

      Ext.scripting
        .executeScript({
          target: { tabId: tab.id },
          func: () => {
            return document.readyState;
          },
        })
        .then((result: any) => {
          // wait for document ready
          if (result && result[0].result === 'complete') {
            clearInterval(timer);
            resolve(true);
          }
        })
        .catch((e: any) => {
          console.log('executeScript err:>> ', e);
          if (timeout < elapsed) {
            reject(new Error('E231: Page load error'));
          }
        });
    }, interval);
  });
}

function callPlayTab(params: any) {

  return askBg(
    'PANEL_CALL_PLAY_TAB',
    {
      payload: {
        command: params.command,
        args: params.args,
      },
    }
  );
}

async function preparePlayTab() {
  await delay(() => { }, 500)
  const tab: any = await getPlayTab()
  await waitForPageLoadComplete(tab)
}

async function sendRunCommand(command: any, retryInfo: any,) {

  const ipcTimeout = 30000
  await callPlayTab({
    command: 'SET_STATUS',
    args: {
      status: CONSTANT.CONTENT_SCRIPT_STATUS.PLAYING,
    },
  });
  await callPlayTab({
    command: 'DOM_READY',
    args: {},
    ipcCallTimeout: ipcTimeout,
  });
  const res: any = await callPlayTab({
    command: 'RUN_COMMAND',
    args: {
      command: {
        ...command,
        extra: {
          ...(command.extra || {}),
          retryInfo,
        },
      },
    },
    ipcCallTimeout: ipcTimeout,
  });

  const secret = res?.data?.secret;

  if (secret) {
    await updateState({ lastCsIpcSecret: secret });
  }

  if (res?.success === false) {
    throw new Error(res.error)
  }

  return res.data;
}

const runCommandWithRetry = async (command: any) => {

  const fn = retry(sendRunCommand, {
    shouldRetry: () => true,
    timeout: 10000
  })
  try {
    return await fn(command)
  } catch (error) {
    if (command.targetOptions && command.targetOptions.length) {
      return sendRunCommand(command, { final: true });
    }
    return Promise.reject(error);
  }

}

const runCommandInPlayTab = async (command: any) => {
  // preparePlayTab 准备playTab, 如果playTab不存在, 则创建playTab
  await preparePlayTab()

  return await runCommandWithRetry(command).catch((e: any) => {
    if (['storeText', 'storeValue', 'storeChecked', 'storeAttribute'].indexOf(command.cmd) !== -1) {
      const value = command.value;
      const LOCATOR_NOT_FOUND = '#LNF';

      return {
        vars: {
          [value]: LOCATOR_NOT_FOUND,
        },
        log: {
          error: e.message,
        },
      };
    }

    // Note: if variable !ERRORIGNORE is set to true,
    // it will just log errors instead of a stop of whole macro
    if (command.extra?.errorIgnore) {
      return {
        log: {
          error: e.message,
        },
      };
    }
    throw new Error(`Error in executeScript: ${e.message}`)
  })

}

const captureScreenshotService = new CaptureScreenshotService({
  captureVisibleTab: (windowId, options) => askBg('PANEL_CAPTURE_VISIBLE_TAB', { windowId, options }) as Promise<string>
})

// 具体的交互命令
export const askBgToRun = (command: any) => {
  const runCommandInPlayTab_result = runCommandInPlayTab(command)
  console.log('runCommandInPlayTab_result:>> ', runCommandInPlayTab_result)
  return runCommandInPlayTab_result
}

// 解析完成后需要执行命令
const runCommand = (command: any, index?: any, parentCommand?: any) => {
  const { cmd, target: target_, value, extra } = command
  const target = target_ as string
  const vars = getVarsInstance()
  const csvStorage = getStorageManager().getCSVStorage()
  const ssStorage = getStorageManager().getScreenshotStorage()

  const playerState = player.getState()

  const result = {
    isFlowLogic: true
  }
  const macroStorage = getStorageManager().getMacroStorage()
  const path = macroStorage.getPathLib()

  const resolvePath = (subpath: any) => {
    subpath = subpath.replace(/\\/g, '/')

    if (subpath.indexOf('/') === 0) {
      return path.normalize(subpath).replace(/^(\/|\\)/, '')
    } else {
      const curMacroRelativePath = getStorageManager().getMacroStorage().relativePath(playerState.extra.macroId)
      const curMacroDirPath = path.dirname(curMacroRelativePath)

      return path.join(curMacroDirPath, subpath)
    }
  }

  switch (cmd) {
    case 'repeatIf':
    case 'elseif':
    case 'if':
    case 'while':
    case 'gotoIf': {
      log(`Executing ${cmd}: ${target}`)

      return evaluateScript(target).then(
        (result: any) => {
          return {
            condition: result,
            byPass: true
          }
        },
        (e: any) => {
          throw new Error(`Error in condition of ${cmd}: ${e.message}`)
        }
      )
    }
    case 'times': {

      const timesKey = interpreter.getKeyForTimes(index)
      const cursor = 1 + (interpreter.getExtraByKey(timesKey) || 0)
      const max = parseInt(target, 10)

      if (isNaN(max)) {
        throw new Error('target must be a positive number')
      }

      if (max < 1) {
        return {
          condition: false,
          byPass: true
        }
      }

      const shouldContinue = cursor <= max

      if (shouldContinue) {
        interpreter.setExtraByKey(timesKey, cursor)
        vars.set({ '!TIMES': cursor }, true)
      } else {
        interpreter.removeExtraByKey(timesKey)

        const key: any = interpreter.getKeyForSurroundingTimes(index)
        const nextCursor = interpreter.getExtraByKey(key)

        if (nextCursor !== undefined) {
          vars.set({ '!TIMES': nextCursor }, true)
        } else {
          vars.set({ '!TIMES': cursor }, true)
        }
      }

      return {
        condition: shouldContinue,
        byPass: true
      }
    }
    case 'forEach': {

      const forEachKey = `forEach_${index}`
      let tagHasBreak = interpreter.hasBreak(index)
      if (tagHasBreak) {
        interpreter.removeBreak(index)
      }

      const current = interpreter.getExtraByKey(forEachKey)
      const cursor = tagHasBreak ? 0 : 1 + (current === undefined ? -1 : current)
      const list = vars.get(target)

      if (!Array.isArray(list)) {
        throw new Error('target must be an array')
      }

      const len = list.length
      const shouldContinue = cursor < len
      const varsToSet = shouldContinue ? { [value]: list[cursor] } : null

      vars.set(
        {
          '!FOREACH': cursor
        },
        true
      )

      if (!shouldContinue) {
        interpreter.removeExtraByKey(forEachKey)
      } else {
        interpreter.setExtraByKey(forEachKey, cursor)
      }

      return {
        vars: varsToSet,
        condition: shouldContinue,
        byPass: true
      }
    }
    case 'assert':
    case 'verify': {
      const isAssert = cmd === 'assert'
      const varName = target

      if (!varName || !varName.length) {
        throw new Error(`${cmd}: target is required as variable name`)
      }

      const actualString = `${vars.get(varName)}`
      const expectedString = `${value}`

      if (actualString === expectedString) {
        return { byPass: true }
      }

      const message = `Expected variable ${varName} to be ${expectedString}, but it is ${actualString}`

      if (isAssert) {
        throw new Error(message)
      }

      return {
        byPass: true,
        log: {
          error: message
        }
      }
    }
    case 'executeScript_Sandbox':
    case 'executeAsyncScript_Sandbox': {
      const code = `;(function () { ${target} })();`

      return evaluateScript(code)
        .then((result: any) => {
          if (value && value.length) {
            return {
              byPass: true,
              vars: {
                [value]: result
              }
            }
          }

          return {
            byPass: true
          }
        })
        .catch((e: any) => {
          throw new Error(`Error in executeScript_Sandbox code: ${e.message}`)
        })
    }
    case 'run': {
      const state = store.getState()
      const macroRelativePath = resolvePath(target)
      const macroNode = findMacroNodeWithCaseInsensitiveRelativePath(state, macroRelativePath)
      const macroStorage = getStorageManager().getMacroStorage()

      return macroStorage.read((macroNode && macroNode.fullPath) || macroRelativePath, 'Text').then((macro: any) => {
        const openCmd: any = macro.data.commands.find(
          (command: any) => command.cmd.toLowerCase() === 'open' || command.cmd.toLowerCase() === 'openBrowser'
        )
        const playerState = act.commonPlayerState(
          store.getState(),
          {
            extra: {
              id: macro.id
            },
            mode: PLAY_MODE.STRAIGHT,
            startIndex: 0,
            startUrl: openCmd ? openCmd.target : null,
            resources: macro.data.commands,
            postDelay: state.config.playCommandInterval * 1000,
            isStep: player.getState().isStep,
            loopsCursor: 1,
            loopsStart: 1,
            loopsEnd: 1
          },
          macro.id,
          macro.name
        )

        return delay(() => { }, 10)
          .then(() =>
            callStack.call(macro, {
              playerState,
              status: MacroStatus.Running,
              nextIndex: 0,
              commandResults: []
            })
          )
          .then(() => {
            store.dispatch(act.updateMacroPlayStatus(macro.id, MacroResultStatus.SUCCESS))

            return {
              byPass: true
            }
          })
      })
    }
    case 'store': {
      let target_ = target
      const tessLang: OCRLanguage = target.toLowerCase() as OCRLanguage

      // if value == '!ocrlanguage' and engine == 98 (tesseract)
      // then transform the target (eg. ger) to tesseract language code (deu)
      if (value == '!ocrlanguage') {
        let engine_ = store.getState().config.ocrEngine
        if (engine_ == 98) {
          target_ = convertOcrLanguageToTesseractLanguage(tessLang)
        }
      }

      return {
        byPass: true,
        vars: {
          [value]: target_
        }
      }
    }
    case 'echo': {
      const extra = (function () {
        if (value === '#shownotification') {
          return { options: { notification: true } }
        }
        if (value) {
          return { options: { color: value } }
        }
        return {}
      })()

      // console.log('echo target :>> ', target);

      return {
        byPass: true,
        log: {
          info: target,
          ...extra
        }
      }
    }
    case 'throwError': {
      throw new Error(target)
    }
    case 'pause': {
      const n = parseInt(target)

      if (!target || !target.length || n === 0) {
        return {
          byPass: true,
          control: {
            type: 'pause'
          }
        }
      }

      if (isNaN(n) || n < 0) {
        throw new Error('target of pause command must be a positive integer')
      }

      const currentPlayUID = player.getPlayUID()

      return withCountDown({
        timeout: n,
        interval: 1000,
        onTick: ({ total, past, cancel }) => {
          if (store.getState().player.status !== CONSTANT.PLAYER_STATUS.PLAYING) {
            return cancel()
          }

          if (!player.checkPlayUID(currentPlayUID)) {
            return cancel()
          }

          store.dispatch(act.setTimeoutStatus({
            past,
            total,
            type: 'pause'
          })
          )
        }
      }).then(() => ({ byPass: true }))
    }
    case 'OCRSearch': {
      guardOcrSettings({ store })

      if (!value || !value.length) {
        throw new Error('value is required')
      }
      const curent_cmd = localStorage.curent_cmd
      const lang = vars.get('!ocrLanguage').toLowerCase()
      const engine = vars.get('!ocrEngine') || store.getState().config.ocrEngine
      const scale = vars.get('!ocrScale')
      const isTable = vars.get('!ocrTableExtraction')
      const timeout = vars.get('!TIMEOUT_WAIT') * 1000
      const searchArea = vars.get('!visualSearchArea') || 'viewport'
      const storedImageRect = vars.get('!storedImageRect')
      const ocrApiTimeout = config.ocr.apiTimeout
      const [str, index, hasPos] = (() => {
        let match = target.match(/^(.+)@POS=(\d+)$/i)
        if (!match) return [target, 0, false]
        return [match[1], parseInt(match[2]) - 1, true]
      })()
      const isLog = command.mode_type != undefined && command.mode_type == 'local' ? 'NA' : true
      const run = () => {
        return getOcrResponse({
          store,
          lang,
          scale,
          engine,
          isTable,
          searchArea,
          storedImageRect,
          ocrApiTimeout,
          isDesktop: false,
          isLog: isLog
        }).then(async ({ response, offset, viewportOffset }: any) => {
          console.log('getOcrResponse response :>> ', response)

          // const state = await getState()
          const viewportBasedParseResults = safeUpdateIn(
            ['[]', 'TextOverlay', 'Lines', '[]', 'Words', '[]'],
            (word: any) => ({
              ...word,
              Top: word.Top + viewportOffset.y,
              Left: word.Left + viewportOffset.x
            }),
            response.ParsedResults
          )
          const documentBasedParseResults = safeUpdateIn(
            ['[]', 'TextOverlay', 'Lines', '[]', 'Words', '[]'],
            (word: any) => ({
              ...word,
              Top: word.Top + offset.y,
              Left: word.Left + offset.x
            }),
            response.ParsedResults
          )
          const searchResult = searchTextInOCRResponse({
            text: str,
            index: index,
            exhaust: true,
            parsedResults: viewportBasedParseResults
          })
          const { hit, all } = searchResult

          console.log('searchResult :>> ', searchResult)
          console.log('command :>> ', command)

          // if (command.extra && command.extra.throwError != undefined && command.extra.throwError != true)
          if (command.mode_type != undefined && command.mode_type == 'local' && command.extra && command.extra.throwError != true) {
            // when press Find button not want retry
            if (searchResult.all.length == 0) {
              throw new Error(`E311: OCR text match for '${str}' not found`)
            }
          }

          const newVars = (() => {
            if (!hit) {
              return {
                [value]: 0,
                '!ocrx': 0,
                '!ocry': 0,
                '!ocrwidth': 0,
                '!ocrheight': 0,
                '!ocr_left_x': 0,
                '!ocr_right_x': 0
              }
            } else {
              const center = ocrMatchCenter(hit)

              // Note: when '@POS=xx' is used, the possible values are only 0 and 1
              vars.set(
                {
                  '!ocrx': center.x,
                  '!ocry': center.y,
                  '!ocrwidth': center.width,
                  '!ocrheight': center.height,
                  '!ocr_left_x': center.x - center.width / 2,
                  '!ocr_right_x': center.x + center.width / 2
                },
                true
              )

              return {
                [value]: hasPos ? 1 : all.length,
                '!ocrx': center.x,
                '!ocry': center.y,
                '!ocrwidth': center.width,
                '!ocrheight': center.height,
                '!ocr_left_x': center.x - center.width / 2,
                '!ocr_right_x': center.x + center.width / 2
              }
            }
          })()

          const textHasWildcard = str.includes('*') || str.includes('?')

          const ocrMatches = [
            // All words identified by OCR into one group
            {
              similarity: 1,
              highlight: OcrHighlightType.Identified,
              words: allWordsWithPosition(
                documentBasedParseResults,
                flatten(all.map((item: any) => item.words.map((word: any) => word.position)))
              )
            },
            // All matched and one top matched
            ...compose(
              all[index]
                ? setIn([index, 'highlight'], textHasWildcard ? OcrHighlightType.WildcardTopMatched : OcrHighlightType.TopMatched)
                : (x: any) => x,
              setIn(['[]', 'highlight'], textHasWildcard ? OcrHighlightType.WildcardMatched : OcrHighlightType.Matched),
              updateIn(['[]', 'words', '[]', 'word'], (word: any) => ({
                ...word,
                Top: word.Top + offset.y - viewportOffset.y,
                Left: word.Left + offset.x + viewportOffset.x
              }))
            )(all)
          ]

          if (
            extra &&
            extra.debugVisual &&
            curent_cmd != 'OCRExtractbyTextRelative' &&
            curent_cmd != 'visionLimitSearchAreabyTextRelative'
          ) {
            // show overlay on website
            askBg('PANEL_HIGHLIGHT_OCR_MATCHES', {
              ocrMatches,
              isDesktop: false,
              screenAvailableSize: {
                width: screen.availWidth,
                height: screen.availHeight
              },
              localStorage: cloneSerializableLocalStorage(localStorage)
            })
          } else {
            // Note: store ocrMatches for preview window when Find button click (OCRExtractbyTextRelative);
            localStorage.setItem('ocrMatches_preview', JSON.stringify(ocrMatches))
          }

          const pScaleFactor = Promise.resolve(1)

          // Note: In desktop mode, `!ocrx`, `!ocry` and `best` should be desktop coordinates
          return pScaleFactor.then((factor) => {
            return compose(
              newVars['!ocrx'] === undefined ? id : safeUpdateIn(['vars', '!ocrx'], (n: any) => n * factor),
              newVars['!ocry'] === undefined ? id : safeUpdateIn(['vars', '!ocry'], (n: any) => n * factor),
              safeUpdateIn(['best'], (match: any) => (match && match.similarity ? scaleOcrTextSearchMatch(match, factor) : null))
            )({
              vars: newVars,
              byPass: true,
              best: hit
            })
          })
        })
      }

      if (command.mode_type !== undefined && command.mode_type === 'local') {
        const runWithRetry = retry(run, {
          timeout,
          shouldRetry: (e: any) => {
            return store.getState().status === CONSTANT.APP_STATUS.PLAYER && /OCR.*\ not found/.test(e.message)
          },
          retryInterval: (retryCount: number, lastRetryInterval: number) => {
            return 0.5 + 0.25 * retryCount
          },
          onFirstFail: () => {
            startSendingTimeoutStatus(timeout, 'Vision waiting')
          },
          onFinal: () => {
            clearTimerForTimeoutStatus()
          }
        })

        return runWithRetry().catch((e) => {
          // Note: extra.throwError === true, when "Find" button is used
          if (cmd === 'OCRSearch' || (extra && extra.throwError)) {
            throw e
          }

          return {
            byPass: true,
            ...(value && value.length
              ? {
                vars: {
                  [value]: 0
                }
              }
              : {})
          }
        })
      } else {
        return run()
      }
    }
    case 'OCRExtractScreenshot':
      guardOcrSettings({ store })

      if (!value || !value.length) {
        throw new Error('value is required')
      }

      const lang = vars.get('!ocrLanguage').toLowerCase()
      const engine = vars.get('!ocrEngine')
      const scale = vars.get('!ocrScale')
      const ocrApiTimeout = config.ocr.apiTimeout

      let ssImageName = /\.(png)$/i.test(target) ? target : null
      console.log('ssImageName :>> ', ssImageName)

      if (ssImageName) {
        return getStorageManager()
          .getScreenshotStorage()
          .read(ssImageName, 'DataURL')
          .catch(() => {
            // search in vision
            return getStorageManager()
              .getVisionStorage()
              .read(ssImageName, 'DataURL')
              .catch(() => {
                throw new Error(`${ssImageName} not found in Screenshots`)
              })
              .then((dataUrl) => {
                throw new Error(`E505: OCRExtractScreenshot uses images from the Screenshot tab as input`)
              })
          })
          .then((dataUrl) => {
            return dataUrl
          })
          .then((imageDataUrl) => {
            console.log('imageDataUrl :>> ', imageDataUrl)
            if (imageDataUrl) {
              return getOcrResponse({
                store,
                lang,
                scale,
                engine,
                ocrApiTimeout,
                imageDataUrl
              }).then(({ response }: any) => {
                return {
                  byPass: true,
                  vars: {
                    [value]: response.ParsedResults && response.ParsedResults[0] ? response.ParsedResults[0].ParsedText : ''
                  }
                }
              })
            } else {
              throw new Error('Screenshot not found')
            }
          })
      } else {
        throw new Error('target is expected to have *.png extension')
      }
    case 'OCRExtractbyTextRelative': {
      guardOcrSettings({ store })

      const lang = vars.get('!ocrLanguage').toLowerCase()
      const engine = vars.get('!ocrEngine')
      const scale = vars.get('!ocrScale')
      const isTable = vars.get('!ocrTableExtraction')
      const ocrApiTimeout = config.ocr.apiTimeout
      let trimmedTarget: any = target.trim()

      updateState(setIn(['caliber_trget'], trimmedTarget))
      updateState(setIn(['curent_cmd'], cmd))
      localStorage.setItem('curent_cmd', cmd)
      localStorage.setItem('caliber_trget', trimmedTarget)
      localStorage.setItem('isDesktopMode', 'false')
      const defaultWh = 'W30H10'

      const regexForTarget = /^.*#R.*,.*$/

      if (!regexForTarget.test(trimmedTarget)) {
        throw new Error('Wrong input ' + trimmedTarget)
      }

      if (trimmedTarget.indexOf('W') === -1 && trimmedTarget.indexOf('H') === -1) {
        trimmedTarget = trimmedTarget + defaultWh
      }

      if (trimmedTarget.indexOf('#R') == -1 || trimmedTarget.includes(' ') || trimmedTarget.includes('X')) {
        throw new Error('Wrong input ' + trimmedTarget)
      }
      if (trimmedTarget.indexOf('W') === -1 || trimmedTarget.indexOf('H') === -1) {
        throw new Error('Wrong input ' + trimmedTarget)
      }

      let indexW = trimmedTarget.indexOf('W')
      let indexH = trimmedTarget.indexOf('H')

      if (indexW !== -1 && indexH !== -1 && indexW < indexH) {
        log("'W' appears first in the string")
      } else {
        // Note: reason for this changes is we are using regex accordingly in getCoordinates function for commands.
        // Extract the values of 'W' and 'H' from the string
        let wValue = trimmedTarget.match(/W(\d+)/)[1]
        let hValue = trimmedTarget.match(/H(\d+)/)[1]

        trimmedTarget = trimmedTarget.replace(/W(\d+)/, `H${hValue}`)

        trimmedTarget = trimmedTarget.replace(/H(\d+)/, `W${wValue}`)

        localStorage.setItem('caliber_trget', trimmedTarget)
      }

      return askBg('PANEL_CLEAR_OCR_MATCHES_ON_PLAYING_PAGE')
        .catch(() => { })
        .then(() => delay(() => { }, 1000))
        .then(() => {
          return Promise.all([
            interpretCsFreeCommands({
              ...command,
              cmd: 'OCRSearch',
              target: trimmedTarget.split('#')[0],
              value: '__ocrResult__'
            }),
            Promise.resolve(1)
          ])
        })
        .then(([result, scalingFactor]) => {
          store.dispatch(act.setOcrInDesktopMode(false));

          const { best } = result
          if (!best) throw new Error(`E311: No OCR text match found for '${target}'`)

          let xC = result.best.words[0].word.Left
          let yC = result.best.words[0].word.Top

          let yD = 0
          let xD = 0

          function getCoordinates(str: string | null) {

            let regex = /R(-?\d+),(-?\d+)W(-?\d+)H(-?\d+)/
            let matches: any = str?.match(regex) || []

            let x = parseInt(matches[1])
            let y = parseInt(matches[2])
            let W = parseInt(matches[3])
            let H = parseInt(matches[4])

            return [x, y, W, H]
          }

          const cal_tragte = localStorage.getItem('caliber_trget') ? localStorage.getItem('caliber_trget') : ''
          let caliberTick: any = cal_tragte

          if (caliberTick.indexOf('W') == -1 || caliberTick.indexOf('H') == -1) {
            caliberTick = caliberTick + 'W30H10'
          }

          function getTickCounter(str: string | null) {
            function getNumberSet(num: string | number, type: string) {
              if (parseInt(num as string) > 0 && type == 'X') {
                return ['>', parseInt(num as string)]
              } else if (parseInt(num as string) < 0 && type == 'X') {
                return ['<', parseInt(String(num).replace('-', ''))]
              } else if (parseInt(num as string) > 0 && type == 'Y') {
                return ['^', parseInt(num as string)]
              } else {
                return ['v', parseInt(String(num).replace('-', ''))]
              }
            }

            const nums = getCoordinates(str)
            const [x1, y1] = getNumberSet(nums[0], 'X')
            let [x2, y2] = getNumberSet(nums[1], 'Y')
            let valueObj: any = {}
            valueObj[x1] = y1
            valueObj[x2] = y2
            return valueObj
          }

          const countCalObj = getTickCounter(caliberTick)


          let ocrCalibration = store.getState().config.ocrCalibration_internal ? store.getState().config.ocrCalibration_internal : 6
          const isDesktopMode = false
          if (!isDesktopMode) {
            ocrCalibration = 7
          }

          for (let x in countCalObj) {
            if (x === 'v' || x === 'v') {
              yD += yC + ocrCalibration * countCalObj[x] // down (add in y offset)
            }
            if (x === '>') {
              xD += xC + ocrCalibration * countCalObj[x] // right (add in x offset)
            }
            if (x === '<') {
              xD += xC - ocrCalibration * countCalObj[x] // left (minus in x offset)
            }
            if (x === '^') {
              yD += yC - ocrCalibration * countCalObj[x] // up (minus in y offset)
            }
          }

          const all_nums = getCoordinates(caliberTick)
          const rectTop = yD
          const rectLeft = xD
          const rectWidth = ocrCalibration * all_nums[2]
          const rectHeight = ocrCalibration * all_nums[3]

          return withVisualHighlightHidden(() => {
            return getOcrResponse({
              store,
              lang,
              scale,
              engine,
              isTable,
              ocrApiTimeout,
              searchArea: 'rect',
              storedImageRect: {
                x: rectLeft / scalingFactor,
                y: rectTop / scalingFactor,
                width: rectWidth / scalingFactor,
                height: rectHeight / scalingFactor
              },
              isDesktop: false
            }).then(({ response, offset, viewportOffset }: any) => {
              const documentBasedParseResults = safeUpdateIn(
                ['[]', 'TextOverlay', 'Lines', '[]', 'Words', '[]'],
                (word: { Top: any; Left: any }) => ({
                  ...word,
                  Top: word.Top + offset.y,
                  Left: word.Left + offset.x
                }),
                response.ParsedResults
              )

              let ocrMatches = [
                // All words identified by OCR into one group
                {
                  similarity: 1,
                  highlight: OcrHighlightType.Matched,
                  words: allWordsWithPosition(documentBasedParseResults, [])
                }
              ]
              // Note: This code is now needed for preview window for command OCRExtractbyTextRelative.
              if (extra && extra.debugVisual) {
                // show overlay on website
                let ocrMatches_preview = localStorage.getItem('ocrMatches_preview')
                ocrMatches = ocrMatches_preview ? JSON.parse(ocrMatches_preview) : ocrMatches
                askBg('PANEL_HIGHLIGHT_OCR_MATCHES', {
                  ocrMatches,
                  isDesktop: false,
                  screenAvailableSize: {
                    width: screen.availWidth,
                    height: screen.availHeight
                  },
                  localStorage: cloneSerializableLocalStorage(localStorage)
                })
              }

              return {
                byPass: true,
                vars: {
                  [value]: response.ParsedResults && response.ParsedResults[0] ? response.ParsedResults[0].ParsedText : ''
                }
              }
            })
          })
        })
    }
    case 'localStorageExport': {
      const deleteAfterExport = /\s*#DeleteAfterExport\s*/i.test(value)

      if (/^\s*log\s*$/i.test(target)) {
        const text = store.getState().logs.map(renderLog).join('\n')
        downloadByBlob(new Blob([text]), 'uivision_log.txt')

        if (deleteAfterExport) {
          store.dispatch(act.clearLogs())
        }
        return result
      }

      if (/\.csv$/i.test(target)) {
        return csvStorage.exists(target).then((existed) => {
          if (!existed) throw new Error(`${target} doesn't exist`)

          return csvStorage.read(target, 'Text').then((text: any) => {
            downloadByBlob(new Blob([text]), target)

            if (deleteAfterExport) {
              csvStorage.remove(target).then(() => store.dispatch(act.listCSV()))
            }

            return result
          })
        })
      }

      if (/\.png$/i.test(target)) {
        return ssStorage.exists(target).then((existed) => {
          if (!existed) throw new Error(`${target} doesn't exist`)

          return ssStorage.read(target, 'ArrayBuffer').then((buffer) => {
            downloadByBlob(new Blob([new Uint8Array(buffer as ArrayBuffer)]), target)

            if (deleteAfterExport) {
              ssStorage.remove(target).then(() => store.dispatch(act.listScreenshots()))
            }

            return result
          })
        })
      }

      throw new Error(`${target} doesn't exist`)
    }
    case 'bringBrowserToForeground': {
      let shouldHide

      try {
        shouldHide = target === '' || target === undefined ? false : !strictParseBoolLike(target)
      } catch (e) {
        throw new Error('E310: Invalid target for bringBrowserToForeground. It should be true / false or leave it blank')
      }

      const p = shouldHide ? askBg('PANEL_MINIMIZE_ALL_WINDOWS_BUT_PANEL') : askBg('PANEL_BRING_PLAYING_WINDOW_TO_FOREGROUND')

      return p.then(() => ({ byPass: true }))
    }

    case 'bringIDEandBrowserToBackground': {
      return askBg('PANEL_MINIMIZE_ALL_WINDOWS').then(() => ({ byPass: true }))
    }
    case 'setWindowSize':
    case 'resize': {
      const [strWidth, strHeight] = (() => {
        if (cmd === 'resize') {
          if (!/\s*\d+@\d+\s*/.test(target)) {
            throw new Error(`E365: Syntax for target of resize command is x@y, e.g. 800@600`)
          }
          return target.split('@')
        } else {
          if (!/\s*\d+x\d+\s*/i.test(target)) {
            throw new Error(`E366: Syntax for target of setWindowSize command is WidthxHeight, e.g. 800x600`)
          }
          return target.split(/x/i)
        }
      })()
      const width = parseInt(strWidth, 10)
      const height = parseInt(strHeight, 10)

      log('resize', width, height)
      return askBg('PANEL_RESIZE_PLAY_TAB', {
        viewportSize: { width, height },
        screenAvailableRect: {
          x: (window.screen as any).availLeft,
          y: (window.screen as any).availTop,
          width: (window.screen as any).availWidth,
          height: (window.screen as any).availHeight
        }
      })
        .then(({ actual, desired, diff }: any) => {
          if (diff.length === 0) return { byPass: true }

          return {
            byPass: true,
            log: {
              warning: `W367: Only able to resize it to ${actual.width}@${actual.height}, given ${desired.width}@${desired.height}`
            }
          }
        })
    }
    case 'captureScreenshot': {
      const fileName = ensureExtName('.png', target)
      const devicePixelRatio = window.devicePixelRatio

      return getState()
        .then((state: { tabIds: { toPlay: number } }) => {
          return activateTab(state.tabIds.toPlay, true)
            .then(() => delay(() => { }, CONSTANT.SCREENSHOT_DELAY))
            .then(() => {
              return captureScreenshotService.saveScreen(
                getStorageManager().getScreenshotStorage(),
                state.tabIds.toPlay,
                fileName,
                devicePixelRatio
              )
            })
        })
        .then(({ fileName, url }: any) => ({
          screenshot: {
            url,
            name: fileName
          },
          byPass: true
        }))
    }
    case 'captureEntirePageScreenshot': {
      const originalFileName = ensureExtName('.png', target)
      // replace % with _ to avoid error in file name
      const fileName = originalFileName.replace(/%/g, '_')

      return getPlayTab()
        .then((tab: any) => {
          return captureScreenshotService.saveFullScreen(getStorageManager().getScreenshotStorage(), tab.id, fileName, {
            startCapture: () => {
              return askPlayTab('START_CAPTURE_FULL_SCREENSHOT', {}, CONSTANT.CS_IPC_TIMEOUT) as any
            },
            endCapture: (pageInfo) => {
              return askPlayTab('END_CAPTURE_FULL_SCREENSHOT', { pageInfo }, CONSTANT.CS_IPC_TIMEOUT) as any
            },
            scrollPage: (offset) => {
              return askPlayTab('SCROLL_PAGE', { offset }, CONSTANT.CS_IPC_TIMEOUT) as any
            }
          })
        })
        .then(({ fileName, url }) => ({
          screenshot: {
            url,
            name: fileName
          },
          byPass: true
        }))
    }
    case 'selectWindow': {
      const p = (() => {
        switch (target && target.toUpperCase()) {
          case 'TAB=CLOSEALLOTHER':
            return askBg('PANEL_CLOSE_OTHER_TABS', {})

          case 'TAB=CLOSE':
            return askBg('PANEL_CLOSE_CURRENT_TAB', {})
          default:
            // const [type, locator] = splitIntoTwo('=', target)
            return askBg('PANEL_SELECT_WINDOW', { target, value })
        }
      })()

      // Note: let `selectWindow` pass through cs and back to background,
      // to keep the flow more consistent with the other commands
      return p.then(() => {
        return getPlayTab().then((tab: any) => {
          return {
            byPass: true,
            pageUrl: tab.url,
            tabIndex: tab.index
          }
        })
      })
    }
    case 'open': {
      return Ext.tabs.create({ url: target, active: true }).then((tab: any) => {
        return {
          byPass: true,
          pageUrl: tab.url,
          tabIndex: tab.index
        }
      })
    }
    default:
      return undefined
  }
}

export const interpretCsFreeCommands = (command: any, index?: any, parentCommand?: any): any => {
  const { cmd } = command

  const vars = getVarsInstance()
  console.log('#233 interpretCsFreeCommands vars:>> ', vars.dump())

  console.log('cmd:>> ==`' + cmd + '`')

  return runCommand(command, index, parentCommand)
}

// 判断CSV文件是否存在
const assertCsvExist = (target: string) => {
  return getStorageManager().getCSVStorage().exists(target).then((isExisted: boolean) => {
    if (isExisted) {
      if (!vars.get('!CsvReadLineNumber')) {
        vars.set({ '!CsvReadLineNumber': 1 }, true)
      }
      return
    }

    vars.set({ '!CsvReadStatus': 'FILE_NOT_FOUND' }, true)

    let errMsg = `E325: csv file '${target}' does not exist`

    if ((pathPosix.isAbsolute(target) || pathWin32.isAbsolute(target))) {
      errMsg += '. Full path works only in hard-drive mode.'
    }

    throw new Error(errMsg)
  })
}

/**
 * 解析CVS命令
 * csvRead
 * csvSave
 * csvReadArray
 * csvSaveArray
 */
export const interpretCSVCommands = (command: Record<string, any>, index: number) => {

  const { cmd, target, value } = command

  switch (cmd) {
    // 读取csv文件 target csv地址 value 空
    case 'csvRead': {
      if (value && value.length > 0) {
        store.dispatch(act.addLog('warning', 'csvRead: Value field should be empty (not used)'))
      }

      return assertCsvExist(target).then(() => {
        return getStorageManager().getCSVStorage()
          .read(target, 'Text')
          .then(parseFromCSV)
          .then((rows: any[]) => {

            const index = vars.get('!CsvReadLineNumber') - 1;// 索引从0开始
            const row = rows[index]

            if (index >= rows.length) {
              vars.set({ '!CsvReadStatus': 'END_OF_FILE' }, true)
              throw new Error('end of csv file reached')
            } else {
              vars.set(
                {
                  '!CsvReadStatus': 'OK',
                  '!CsvReadMaxRow': rows.length
                },
                true
              )
            }

            vars.clear(/^!COL\d+$/i);// 清空旧数据

            // 保存到变量中
            row.forEach((data: string, i: number) => {
              vars.set({ [`!COL${i + 1}`]: data })
            })

            return {
              isFlowLogic: true // 解析完毕，无需执行
            }
          })
      })
    }

    // 命令csvSave 追加一行
    case 'csvSave': {
      const csvLine = vars.get('!CSVLINE')

      if (!csvLine || !csvLine.length) {
        throw new Error('No data to save to csv')
      }

      return stringifyToCSV([csvLine])
        .then((newLineText: any) => {
          const fileName = /\.csv$/i.test(target) ? target : target + '.csv'

          return getStorageManager().getCSVStorage().exists(fileName).then((isExisted) => {
            if (!isExisted) {
              return getStorageManager().getCSVStorage().write(fileName, new Blob([newLineText]))
            }

            return getStorageManager().getCSVStorage().read(fileName, 'Text').then((originalText) => {
              const text = (originalText + '\n' + newLineText).replace(/\n+/g, '\n')
              return getStorageManager().getCSVStorage().overwrite(fileName, new Blob([text]))
            })
          })
        })
        .then(() => {
          vars.clear(/^!CSVLINE$/)
          store.dispatch(act.listCSV());// 更新csvlist
        })
        .then(() => ({
          isFlowLogic: true
        }))
    }

    // 命令:csvReadArray将读取的结果放到value中
    case 'csvReadArray': {
      if (!value || !value.length) {
        throw new Error('E326: Please provide variable name as value')
      }

      return assertCsvExist(target).then(() => {
        return getStorageManager().getCSVStorage()
          .read(target, 'Text')
          .then(parseFromCSV)
          .then((rows: any[]) => {
            vars.set(
              {
                '!CsvReadStatus': true,
                '!CsvReadMaxRow': rows.length
              },
              true
            )

            return {
              byPass: true, // 解析完成，后续变量自己处理
              vars: {
                [value]: rows
              }
            }
          },
            (e) => {
              vars.set({ '!CsvReadStatus': false }, true)
              return Promise.reject(e)
            }
          )
      })
    }
    // 命令csvSaveArray将二维数组保存到csv文件中
    case 'csvSaveArray': {
      if (!value || !value.length) {
        throw new Error('E327: Please provide csv file name as value')
      }

      if (!target || !target.length) {
        throw new Error('E328: Please provide array variable name as target')
      }

      const arr = vars.get(target)

      if (!arr) {
        throw new Error(`E329: No variable found with name '${target}'`)
      }

      const isValidCsvArray = Array.isArray(arr) && Array.from(arr).every((item) => Array.isArray(item))

      if (!isValidCsvArray) {
        throw new Error('E330: Array must be two dimensional array')
      }

      return stringifyToCSV(arr)
        .then((csvText: any) => {
          const fileName = /\.csv$/i.test(value) ? value : value + '.csv'
          return getStorageManager().getCSVStorage().overwrite(fileName, new Blob([csvText]))
        })
        .then(() => {
          store.dispatch(act.listCSV())
        })
        .then(() => ({
          isFlowLogic: true
        }))
    }

    default:
      return undefined
  }
}

// 解析特殊命令
export const interpretSpecialCommands = (command: Record<string, any>, index: number) => {
  const commandRunners = [interpretCSVCommands, interpretCsFreeCommands]
  return commandRunners.reduce((prev, cur) => {
    if (prev !== undefined) return prev
    return cur(command, index)
  }, undefined)
}